﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Diagnostics;
using System.Reflection;
using System.Runtime.Versioning;
using Microsoft.DotNet.Cli.Commands.Workload;
using Microsoft.DotNet.Cli.Utils.Extensions;
using Microsoft.Win32.Msi;

namespace Microsoft.DotNet.Cli.Installer.Windows;

/// <summary>
/// Base class for Windows installer components.
/// </summary>
/// <remarks>
/// Creates a new <see cref="InstallerBase"/> instance using the specified elevation context and logger.
/// </remarks>
/// <param name="elevationContext"></param>
/// <param name="logger"></param>
/// <param name="verifySignatures">Determines whether MSI signatures should be verified</param>
[SupportedOSPlatform("windows")]
internal abstract class InstallerBase(InstallElevationContextBase elevationContext, ISetupLogger logger, bool verifySignatures)
{
    /// <summary>
    /// The current process.
    /// </summary>
    public static readonly Process CurrentProcess;

    /// <summary>
    /// A lower invariant string representation of the processor architecture of the running .NET host, e.g.,
    /// "x86" or "arm64".
    /// </summary>
    public static readonly string HostArchitecture = RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();

    /// <summary>
    /// The dispatcher used for processing install commands using IPC.
    /// </summary>
    protected InstallMessageDispatcher Dispatcher => ElevationContext.Dispatcher;

    /// <summary>
    /// The elevation context associated with the process.
    /// </summary>
    protected InstallElevationContextBase ElevationContext
    {
        get;
    } = elevationContext;

    /// <summary>
    /// Returns true if the current process is 64-bit.
    /// </summary>
    protected readonly bool Is64BitProcess = Environment.Is64BitProcess;

    /// <summary>
    /// <see langword="true"/> if this is an install client; <see langword="false"/> otherwise.
    /// </summary>
    protected bool IsClient => ElevationContext.IsClient;

    /// <summary>
    /// <see langword="true"/> if the <see cref="System.Security.Principal"/> associated
    /// with this process belongs to the administrators group.
    /// </summary>
    protected bool IsElevated => ElevationContext.IsElevated;

    /// <summary>
    /// Provides access to the underlying setup log.
    /// </summary>
    protected readonly ISetupLogger Log = logger;

    /// <summary>
    /// The parent process of this process.
    /// </summary>
    public static readonly Process ParentProcess;

    /// <summary>
    /// Gets the processor architecture.
    /// </summary>
    protected static readonly string ProcessorArchitecture;

    /// <summary>
    /// Queries WUA to determine if the system has a pending reboot.
    /// </summary>
    protected readonly bool RebootPending = WindowsUtils.RebootRequired();

    /// <summary>
    /// <see langword="true"/> if an operation returned <see cref="Error.SUCCESS_REBOOT_INITIATED"/> or <see cref="Error.SUCCESS_REBOOT_REQUIRED"/>.
    /// </summary>
    public bool Restart
    {
        get;
        private set;
    }

    /// <summary>
    /// The name of the SDK directory, e.g. 6.0.100.
    /// </summary>
    protected static string SdkDirectory => Path.GetFileName(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location));

    /// <summary>
    /// Gets whether signatures for workload packages and installers should be verified.
    /// </summary>
    protected bool VerifySignatures
    {
        get;
    } = verifySignatures;

    /// <summary>
    /// Starts an elevated process to perform privileged operations.
    /// </summary>
    protected void Elevate()
    {
        ElevationContext.Elevate();
    }

    /// <summary>
    /// Checks the specified error code to determine whether it indicates a success result. If not, additional extended information
    /// is retrieved before throwing a <see cref="WorkloadException"/>.
    /// 
    /// The <see cref="Restart"/> property will be set to <see langword="true" /> if the error is either <see cref="Error.SUCCESS_REBOOT_INITIATED"/>
    /// or <see cref="Error.SUCCESS_REBOOT_REQUIRED"/>.
    /// </summary>
    /// <param name="error">The error code to check.</param>
    /// <param name="message">The message to include the exception.</param>
    /// <exception cref="WorkloadException" />
    protected void ExitOnError(uint error, string message)
    {
        if (!Error.Success(error))
        {
            throw new WorkloadException($"{message} Error: 0x{error:x8}, {Marshal.GetPInvokeErrorMessage((int)error)}");
        }

        // Once set to true, we retain restart information for the duration of the underlying command.
        Restart |= error == Error.SUCCESS_REBOOT_INITIATED || error == Error.SUCCESS_REBOOT_REQUIRED;
    }

    /// <summary>
    /// Reports the specified error if the provided condition evaluates to <see langword="true"/>.
    /// </summary>
    /// <param name="condition">The condition to evaluate before reporting the error.</param>
    /// <param name="error">The error code to report if the condition is met.</param>
    /// <param name="message">The message associated with the error.</param>
    protected void ExitOnError(bool condition, uint error, string message)
    {
        if (condition)
        {
            ExitOnError(error, message);
        }
    }

    /// <summary>
    /// Throws an exception if the HRESULT of the response message indicates
    /// a failure.
    /// </summary>
    /// <param name="response">The response message to examine.</param>
    /// <exception cref="WorkloadException"/>
    protected static void ExitOnFailure(InstallResponseMessage response, string message)
    {
        if (response.HResult < 0)
        {
            throw new WorkloadException(string.IsNullOrWhiteSpace(message) ? response.Message : $"{message} {response.Message}", response.HResult);
        }
    }

    /// <summary>
    /// Logs a message if the specified error code does not indicate a success result. The <see cref="Restart"/>
    /// property will be set to <see langword="true" /> if the error is either <see cref="Error.SUCCESS_REBOOT_INITIATED"/>
    /// or <see cref="Error.SUCCESS_REBOOT_REQUIRED"/>.
    /// 
    /// No exception is thrown by this method. See <see cref="ExitOnError(uint, string)"/> for more detail.
    /// </summary>
    /// <param name="error">The error code to log.</param>
    /// <param name="message">The message to log.</param>
    protected void LogError(uint error, string message)
    {
        if (!Error.Success(error))
        {
            Log?.LogMessage($"{message} Error: 0x{error:x8}.");
        }

        // Once set to true, we retain restart information for the duration of the underlying command.
        Restart |= error == Error.SUCCESS_REBOOT_INITIATED || error == Error.SUCCESS_REBOOT_REQUIRED;
    }

    /// <summary>
    /// Writes the specified exception to the log.
    /// </summary>
    /// <param name="exception">The exception to log.</param>
    protected void LogException(Exception exception)
    {
        Log?.LogMessage($"Exception: {exception.Message}, HResult: 0x{exception.HResult:x8}.");
        Log?.LogMessage($"{exception.StackTrace}");
    }

    static InstallerBase()
    {
        CurrentProcess = Process.GetCurrentProcess();
        ParentProcess = CurrentProcess.GetParentProcess();
        ProcessorArchitecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE").ToLowerInvariant();
    }
}
